/*
 * Copyright (c) 1996, 2012, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.beans;

import java.lang.ref.Reference;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

/**
 * An EventSetDescriptor describes a group of events that a given Java
 * bean fires.
 * <P>
 * The given group of events are all delivered as method calls on a single
 * event listener interface, and an event listener object can be registered
 * via a call on a registration method supplied by the event source.
 */
public class EventSetDescriptor extends FeatureDescriptor {

  private MethodDescriptor[] listenerMethodDescriptors;
  private MethodDescriptor addMethodDescriptor;
  private MethodDescriptor removeMethodDescriptor;
  private MethodDescriptor getMethodDescriptor;

  private Reference<Method[]> listenerMethodsRef;
  private Reference<? extends Class<?>> listenerTypeRef;

  private boolean unicast;
  private boolean inDefaultEventSet = true;

  /**
   * Creates an <TT>EventSetDescriptor</TT> assuming that you are
   * following the most simple standard design pattern where a named
   * event &quot;fred&quot; is (1) delivered as a call on the single method of
   * interface FredListener, (2) has a single argument of type FredEvent,
   * and (3) where the FredListener may be registered with a call on an
   * addFredListener method of the source component and removed with a
   * call on a removeFredListener method.
   *
   * @param sourceClass The class firing the event.
   * @param eventSetName The programmatic name of the event.  E.g. &quot;fred&quot;. Note that this
   * should normally start with a lower-case character.
   * @param listenerType The target interface that events will get delivered to.
   * @param listenerMethodName The method that will get called when the event gets delivered to its
   * target listener interface.
   * @throws IntrospectionException if an exception occurs during introspection.
   */
  public EventSetDescriptor(Class<?> sourceClass, String eventSetName,
      Class<?> listenerType, String listenerMethodName)
      throws IntrospectionException {
    this(sourceClass, eventSetName, listenerType,
        new String[]{listenerMethodName},
        Introspector.ADD_PREFIX + getListenerClassName(listenerType),
        Introspector.REMOVE_PREFIX + getListenerClassName(listenerType),
        Introspector.GET_PREFIX + getListenerClassName(listenerType) + "s");

    String eventName = NameGenerator.capitalize(eventSetName) + "Event";
    Method[] listenerMethods = getListenerMethods();
    if (listenerMethods.length > 0) {
      Class[] args = getParameterTypes(getClass0(), listenerMethods[0]);
      // Check for EventSet compliance. Special case for vetoableChange. See 4529996
      if (!"vetoableChange".equals(eventSetName) && !args[0].getName().endsWith(eventName)) {
        throw new IntrospectionException("Method \"" + listenerMethodName +
            "\" should have argument \"" +
            eventName + "\"");
      }
    }
  }

  private static String getListenerClassName(Class<?> cls) {
    String className = cls.getName();
    return className.substring(className.lastIndexOf('.') + 1);
  }

  /**
   * Creates an <TT>EventSetDescriptor</TT> from scratch using
   * string names.
   *
   * @param sourceClass The class firing the event.
   * @param eventSetName The programmatic name of the event set. Note that this should normally
   * start with a lower-case character.
   * @param listenerType The Class of the target interface that events will get delivered to.
   * @param listenerMethodNames The names of the methods that will get called when the event gets
   * delivered to its target listener interface.
   * @param addListenerMethodName The name of the method on the event source that can be used to
   * register an event listener object.
   * @param removeListenerMethodName The name of the method on the event source that can be used to
   * de-register an event listener object.
   * @throws IntrospectionException if an exception occurs during introspection.
   */
  public EventSetDescriptor(Class<?> sourceClass,
      String eventSetName,
      Class<?> listenerType,
      String listenerMethodNames[],
      String addListenerMethodName,
      String removeListenerMethodName)
      throws IntrospectionException {
    this(sourceClass, eventSetName, listenerType,
        listenerMethodNames, addListenerMethodName,
        removeListenerMethodName, null);
  }

  /**
   * This constructor creates an EventSetDescriptor from scratch using
   * string names.
   *
   * @param sourceClass The class firing the event.
   * @param eventSetName The programmatic name of the event set. Note that this should normally
   * start with a lower-case character.
   * @param listenerType The Class of the target interface that events will get delivered to.
   * @param listenerMethodNames The names of the methods that will get called when the event gets
   * delivered to its target listener interface.
   * @param addListenerMethodName The name of the method on the event source that can be used to
   * register an event listener object.
   * @param removeListenerMethodName The name of the method on the event source that can be used to
   * de-register an event listener object.
   * @param getListenerMethodName The method on the event source that can be used to access the
   * array of event listener objects.
   * @throws IntrospectionException if an exception occurs during introspection.
   * @since 1.4
   */
  public EventSetDescriptor(Class<?> sourceClass,
      String eventSetName,
      Class<?> listenerType,
      String listenerMethodNames[],
      String addListenerMethodName,
      String removeListenerMethodName,
      String getListenerMethodName)
      throws IntrospectionException {
    if (sourceClass == null || eventSetName == null || listenerType == null) {
      throw new NullPointerException();
    }
    setName(eventSetName);
    setClass0(sourceClass);
    setListenerType(listenerType);

    Method[] listenerMethods = new Method[listenerMethodNames.length];
    for (int i = 0; i < listenerMethodNames.length; i++) {
      // Check for null names
      if (listenerMethodNames[i] == null) {
        throw new NullPointerException();
      }
      listenerMethods[i] = getMethod(listenerType, listenerMethodNames[i], 1);
    }
    setListenerMethods(listenerMethods);

    setAddListenerMethod(getMethod(sourceClass, addListenerMethodName, 1));
    setRemoveListenerMethod(getMethod(sourceClass, removeListenerMethodName, 1));

    // Be more forgiving of not finding the getListener method.
    Method method = Introspector.findMethod(sourceClass, getListenerMethodName, 0);
    if (method != null) {
      setGetListenerMethod(method);
    }
  }

  private static Method getMethod(Class<?> cls, String name, int args)
      throws IntrospectionException {
    if (name == null) {
      return null;
    }
    Method method = Introspector.findMethod(cls, name, args);
    if ((method == null) || Modifier.isStatic(method.getModifiers())) {
      throw new IntrospectionException("Method not found: " + name +
          " on class " + cls.getName());
    }
    return method;
  }

  /**
   * Creates an <TT>EventSetDescriptor</TT> from scratch using
   * <TT>java.lang.reflect.Method</TT> and <TT>java.lang.Class</TT> objects.
   *
   * @param eventSetName The programmatic name of the event set.
   * @param listenerType The Class for the listener interface.
   * @param listenerMethods An array of Method objects describing each of the event handling methods
   * in the target listener.
   * @param addListenerMethod The method on the event source that can be used to register an event
   * listener object.
   * @param removeListenerMethod The method on the event source that can be used to de-register an
   * event listener object.
   * @throws IntrospectionException if an exception occurs during introspection.
   */
  public EventSetDescriptor(String eventSetName,
      Class<?> listenerType,
      Method listenerMethods[],
      Method addListenerMethod,
      Method removeListenerMethod)
      throws IntrospectionException {
    this(eventSetName, listenerType, listenerMethods,
        addListenerMethod, removeListenerMethod, null);
  }

  /**
   * This constructor creates an EventSetDescriptor from scratch using
   * java.lang.reflect.Method and java.lang.Class objects.
   *
   * @param eventSetName The programmatic name of the event set.
   * @param listenerType The Class for the listener interface.
   * @param listenerMethods An array of Method objects describing each of the event handling methods
   * in the target listener.
   * @param addListenerMethod The method on the event source that can be used to register an event
   * listener object.
   * @param removeListenerMethod The method on the event source that can be used to de-register an
   * event listener object.
   * @param getListenerMethod The method on the event source that can be used to access the array of
   * event listener objects.
   * @throws IntrospectionException if an exception occurs during introspection.
   * @since 1.4
   */
  public EventSetDescriptor(String eventSetName,
      Class<?> listenerType,
      Method listenerMethods[],
      Method addListenerMethod,
      Method removeListenerMethod,
      Method getListenerMethod)
      throws IntrospectionException {
    setName(eventSetName);
    setListenerMethods(listenerMethods);
    setAddListenerMethod(addListenerMethod);
    setRemoveListenerMethod(removeListenerMethod);
    setGetListenerMethod(getListenerMethod);
    setListenerType(listenerType);
  }

  /**
   * Creates an <TT>EventSetDescriptor</TT> from scratch using
   * <TT>java.lang.reflect.MethodDescriptor</TT> and <TT>java.lang.Class</TT>
   * objects.
   *
   * @param eventSetName The programmatic name of the event set.
   * @param listenerType The Class for the listener interface.
   * @param listenerMethodDescriptors An array of MethodDescriptor objects describing each of the
   * event handling methods in the target listener.
   * @param addListenerMethod The method on the event source that can be used to register an event
   * listener object.
   * @param removeListenerMethod The method on the event source that can be used to de-register an
   * event listener object.
   * @throws IntrospectionException if an exception occurs during introspection.
   */
  public EventSetDescriptor(String eventSetName,
      Class<?> listenerType,
      MethodDescriptor listenerMethodDescriptors[],
      Method addListenerMethod,
      Method removeListenerMethod)
      throws IntrospectionException {
    setName(eventSetName);
    this.listenerMethodDescriptors = (listenerMethodDescriptors != null)
        ? listenerMethodDescriptors.clone()
        : null;
    setAddListenerMethod(addListenerMethod);
    setRemoveListenerMethod(removeListenerMethod);
    setListenerType(listenerType);
  }

  /**
   * Gets the <TT>Class</TT> object for the target interface.
   *
   * @return The Class object for the target interface that will get invoked when the event is
   * fired.
   */
  public Class<?> getListenerType() {
    return (this.listenerTypeRef != null)
        ? this.listenerTypeRef.get()
        : null;
  }

  private void setListenerType(Class<?> cls) {
    this.listenerTypeRef = getWeakReference(cls);
  }

  /**
   * Gets the methods of the target listener interface.
   *
   * @return An array of <TT>Method</TT> objects for the target methods within the target listener
   * interface that will get called when events are fired.
   */
  public synchronized Method[] getListenerMethods() {
    Method[] methods = getListenerMethods0();
    if (methods == null) {
      if (listenerMethodDescriptors != null) {
        methods = new Method[listenerMethodDescriptors.length];
        for (int i = 0; i < methods.length; i++) {
          methods[i] = listenerMethodDescriptors[i].getMethod();
        }
      }
      setListenerMethods(methods);
    }
    return methods;
  }

  private void setListenerMethods(Method[] methods) {
    if (methods == null) {
      return;
    }
    if (listenerMethodDescriptors == null) {
      listenerMethodDescriptors = new MethodDescriptor[methods.length];
      for (int i = 0; i < methods.length; i++) {
        listenerMethodDescriptors[i] = new MethodDescriptor(methods[i]);
      }
    }
    this.listenerMethodsRef = getSoftReference(methods);
  }

  private Method[] getListenerMethods0() {
    return (this.listenerMethodsRef != null)
        ? this.listenerMethodsRef.get()
        : null;
  }

  /**
   * Gets the <code>MethodDescriptor</code>s of the target listener interface.
   *
   * @return An array of <code>MethodDescriptor</code> objects for the target methods within the
   * target listener interface that will get called when events are fired.
   */
  public synchronized MethodDescriptor[] getListenerMethodDescriptors() {
    return (this.listenerMethodDescriptors != null)
        ? this.listenerMethodDescriptors.clone()
        : null;
  }

  /**
   * Gets the method used to add event listeners.
   *
   * @return The method used to register a listener at the event source.
   */
  public synchronized Method getAddListenerMethod() {
    return getMethod(this.addMethodDescriptor);
  }

  private synchronized void setAddListenerMethod(Method method) {
    if (method == null) {
      return;
    }
    if (getClass0() == null) {
      setClass0(method.getDeclaringClass());
    }
    addMethodDescriptor = new MethodDescriptor(method);
    setTransient(method.getAnnotation(Transient.class));
  }

  /**
   * Gets the method used to remove event listeners.
   *
   * @return The method used to remove a listener at the event source.
   */
  public synchronized Method getRemoveListenerMethod() {
    return getMethod(this.removeMethodDescriptor);
  }

  private synchronized void setRemoveListenerMethod(Method method) {
    if (method == null) {
      return;
    }
    if (getClass0() == null) {
      setClass0(method.getDeclaringClass());
    }
    removeMethodDescriptor = new MethodDescriptor(method);
    setTransient(method.getAnnotation(Transient.class));
  }

  /**
   * Gets the method used to access the registered event listeners.
   *
   * @return The method used to access the array of listeners at the event source or null if it
   * doesn't exist.
   * @since 1.4
   */
  public synchronized Method getGetListenerMethod() {
    return getMethod(this.getMethodDescriptor);
  }

  private synchronized void setGetListenerMethod(Method method) {
    if (method == null) {
      return;
    }
    if (getClass0() == null) {
      setClass0(method.getDeclaringClass());
    }
    getMethodDescriptor = new MethodDescriptor(method);
    setTransient(method.getAnnotation(Transient.class));
  }

  /**
   * Mark an event set as unicast (or not).
   *
   * @param unicast True if the event set is unicast.
   */
  public void setUnicast(boolean unicast) {
    this.unicast = unicast;
  }

  /**
   * Normally event sources are multicast.  However there are some
   * exceptions that are strictly unicast.
   *
   * @return <TT>true</TT> if the event set is unicast. Defaults to <TT>false</TT>.
   */
  public boolean isUnicast() {
    return unicast;
  }

  /**
   * Marks an event set as being in the &quot;default&quot; set (or not).
   * By default this is <TT>true</TT>.
   *
   * @param inDefaultEventSet <code>true</code> if the event set is in the &quot;default&quot; set,
   * <code>false</code> if not
   */
  public void setInDefaultEventSet(boolean inDefaultEventSet) {
    this.inDefaultEventSet = inDefaultEventSet;
  }

  /**
   * Reports if an event set is in the &quot;default&quot; set.
   *
   * @return <TT>true</TT> if the event set is in the &quot;default&quot; set.  Defaults to
   * <TT>true</TT>.
   */
  public boolean isInDefaultEventSet() {
    return inDefaultEventSet;
  }

  /*
   * Package-private constructor
   * Merge two event set descriptors.  Where they conflict, give the
   * second argument (y) priority over the first argument (x).
   *
   * @param x  The first (lower priority) EventSetDescriptor
   * @param y  The second (higher priority) EventSetDescriptor
   */
  EventSetDescriptor(EventSetDescriptor x, EventSetDescriptor y) {
    super(x, y);
    listenerMethodDescriptors = x.listenerMethodDescriptors;
    if (y.listenerMethodDescriptors != null) {
      listenerMethodDescriptors = y.listenerMethodDescriptors;
    }

    listenerTypeRef = x.listenerTypeRef;
    if (y.listenerTypeRef != null) {
      listenerTypeRef = y.listenerTypeRef;
    }

    addMethodDescriptor = x.addMethodDescriptor;
    if (y.addMethodDescriptor != null) {
      addMethodDescriptor = y.addMethodDescriptor;
    }

    removeMethodDescriptor = x.removeMethodDescriptor;
    if (y.removeMethodDescriptor != null) {
      removeMethodDescriptor = y.removeMethodDescriptor;
    }

    getMethodDescriptor = x.getMethodDescriptor;
    if (y.getMethodDescriptor != null) {
      getMethodDescriptor = y.getMethodDescriptor;
    }

    unicast = y.unicast;
    if (!x.inDefaultEventSet || !y.inDefaultEventSet) {
      inDefaultEventSet = false;
    }
  }

  /*
   * Package-private dup constructor
   * This must isolate the new object from any changes to the old object.
   */
  EventSetDescriptor(EventSetDescriptor old) {
    super(old);
    if (old.listenerMethodDescriptors != null) {
      int len = old.listenerMethodDescriptors.length;
      listenerMethodDescriptors = new MethodDescriptor[len];
      for (int i = 0; i < len; i++) {
        listenerMethodDescriptors[i] = new MethodDescriptor(
            old.listenerMethodDescriptors[i]);
      }
    }
    listenerTypeRef = old.listenerTypeRef;

    addMethodDescriptor = old.addMethodDescriptor;
    removeMethodDescriptor = old.removeMethodDescriptor;
    getMethodDescriptor = old.getMethodDescriptor;

    unicast = old.unicast;
    inDefaultEventSet = old.inDefaultEventSet;
  }

  void appendTo(StringBuilder sb) {
    appendTo(sb, "unicast", this.unicast);
    appendTo(sb, "inDefaultEventSet", this.inDefaultEventSet);
    appendTo(sb, "listenerType", this.listenerTypeRef);
    appendTo(sb, "getListenerMethod", getMethod(this.getMethodDescriptor));
    appendTo(sb, "addListenerMethod", getMethod(this.addMethodDescriptor));
    appendTo(sb, "removeListenerMethod", getMethod(this.removeMethodDescriptor));
  }

  private static Method getMethod(MethodDescriptor descriptor) {
    return (descriptor != null)
        ? descriptor.getMethod()
        : null;
  }
}
